////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (c) 2009 Gabriel Montagné Láscaris-Comneno and Alberto
//  Brealey-Guzmán.
//
//  Permission is hereby granted, free of charge, to any person obtaining a
//  copy of this software and associated documentation files (the "Software"),
//  to deal in the Software without restriction, including without limitation
//  the rights to use, copy, modify, merge, publish, distribute, sublicense,
//  and/or sell copies of the Software, and to permit persons to whom the
//  Software is furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
//  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
//  DEALINGS IN THE SOFTWARE.
//
////////////////////////////////////////////////////////////////////////////////

package com.rojored.core
{

import flash.events.Event;
import flash.events.EventDispatcher;
import mx.core.IFactory;
import mx.utils.ObjectUtil;

/**
 *  The <code>PooledClassFactory</code> class is meant to be a drop-in
 *  replacement to Flex's <code>ClassFactory</code> class, with additional
 *  object pooling.
 *
 *  <p>The <code>properties</code> property lets you customize how the objects
 *  in the pool are initialized and reset when returned to the pool.</p>
 *
 *  @see mx.core.ClassFactory
 */
public class PooledClassFactory extends EventDispatcher
    implements IFactory, IObjectPool
{

    //--------------------------------------------------------------------------
    //
    //  Class constants
    //
    //--------------------------------------------------------------------------

    /**
     *  Event type for the SIZE_CHANGED event.
     *  // TODO: proper event with old size and size.
     */
    public static const SIZE_CHANGED:String = "sizeChanged";

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor.
     *
     *  @param generator The Class used to generate new objects from this
     *  factory.
     *  @param initialCount Initial number of instances created upon factory
     *  instantiation.
     *  @param properties Name/value pairs that specify the properties and
     *  their values to be set on each new object upon creation or when
     *  returned to the pool.
     */
    public function PooledClassFactory(generator:Class,
                                       initialCount:int = 0,
                                       properties:Object = null)
    {
        super();

        _generator = generator;
        _properties = properties;

        if (initialCount > 0)
            initPool(initialCount);
    }

    //--------------------------------------------------------------------------
    //
    //  Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  List of object instances currently in the pool.
     */
    private var pool:Array = [];

    /**
     *  @private
     *  Whether at least one instance has been lent out.
     */
    private var hasLent:Boolean;

    /**
     *  @private
     *  last size reported.
     */
    private var lastReportedSize:int = 0;

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    //--------------------------------------
    // size
    //--------------------------------------

    [Bindable("sizeChanged")]

    /**
     *  the amount items current on the pool.
     */
    public function get size():int
    {
        return pool ? pool.length : 0;
    }

    //--------------------------------------
    //   generator
    //--------------------------------------

    /**
     *  @private
     *  Storage for the generator property.
     */
    private var _generator:Class;

    /**
     *  The Class that the <code>newInstance()</code> or <code>acquire()</code>
     *  methods use to generate objects from this factory.
     *
     *  <p>Each <code>PooledClassFactory</code> is tied to a class type, since
     *  changing this after the factory's initialization would have all sorts
     *  of implications with already borrowed and currently pooled
     *  instances.</p>
     */
    public function get generator():Class
    {
        return _generator;
    }

    //--------------------------------------
    //   highWatermark
    //--------------------------------------

    /**
     *  @private
     *  Storage for the highWatermark property.
     */
    private var _highWatermark:int = -1;
    private var highWatermarkChanged:Boolean;

    /**
     *  After at least one instance has been acquired from the pool (via the
     *  <code>newInstance()</code> or <code>acquire()</code> methods),
     *  instances will be returned to the pool (via <code>release()</code>)
     *  only if there are less than <code>highWatermark</code> instances in
     *  the pool.
     *
     *  <p>If set to -1 then all instances will be returned to the pool.</p>
     *
     *  @default -1
     */
    public function get highWatermark():int
    {
        return _highWatermark;
    }

    public function set highWatermark(value:int):void
    {
        if (_highWatermark == value)
            return;

        _highWatermark = value;
        highWatermarkChanged = true;

        if (hasLent)
            updatePool();
    }

    //--------------------------------------
    //   properties
    //--------------------------------------

    /**
     *  @private
     *  Storage for the properties property.
     */
    private var _properties:Object;
    private var propertiesChanged:Boolean;

    /**
     *  An object whose name/value pairs specify the properties and their
     *  values to be set on each object returned by the
     *  <code>newInstance()</code> or <code>acquire()</code> methods. These values
     *  are used to refresh objects returned to the pool via the
     *  <code>release()</code> method.
     */
    public function get properties():Object
    {
        return _properties;
    }

    public function set properties(value:Object):void
    {
        // FIXME: What should we do when properties is changed to null?
        // objects in the pool would be initialized to a different set of
        // properties...
        if (ObjectUtil.compare(_properties, value, 0) == 0)
            return;

        _properties = value;
        propertiesChanged = true;
        updatePool();
    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  Creates a new instance of the <code>generator</code> class.
     *
     *  @return A new instance of <code>generator</code>, with properties
     *  initialized from name/value pairs in the <code>properties</code>
     *  property.
     */
    public function newInstance():*
    {
        // might need to resize pool if watermarks have changed...
        if (!hasLent)
        {
            hasLent = true;
            updatePool();
        }

        var instance:* = (pool && pool.length ? pool.shift() : createInstance());
        reportSize();
        return instance;
    }

    /**
     *  Alias for the <code>newInstance()</code> method.
     *
     *  @return A new instance of <code>generator</code>, with properties
     *  initialized from name/value pairs in the <code>properties</code>
     *  property.
     */
    public function acquire():*
    {
        return newInstance();
    }

    /**
     *  Returns an instance to the pool.
     *
     *  @param instance Instance returned to the pool to be reused.
     *
     *  @throws ArgumentError if <code>instance</code> is not an instance of
     *  the <code>generator</code> class.
     */
    public function release(instance:*):void
    {

        if (instance.constructor != generator)
            throw new ArgumentError(
                "the instance parameter is not of type " + generator
                );

        if (_highWatermark == -1 || _highWatermark > pool.length)
            pool.push(resetObject(instance));

        reportSize();
    }

    /**
     *  Resets objects to be returned to the pool.
     *
     *  @param instance Instance that will have its properties reset from the
     *  name/value pairs in <code>properties</code>.
     *  @return Instance received as <code>instance</code> parameter, with its
     *  properties updated according to name/value pairs in
     *  <code>properties</code>.
     */
    protected function resetObject(instance:*):*
    {
        if (_properties)
            for (var p:String in _properties)
                instance[p] = _properties[p];

        return instance;
    }

    /**
     *  Creates a new <code>generator</code> object.
     */
    protected function createInstance():*
    {

        var instance:* = new generator();

        if (_properties)
            instance = resetObject(instance);

        reportSize();
        return instance;

    }

    /**
     *  @private
     *  Inits the pool, creating the first initialCount number of instances.
     */
    private function initPool(initialCount:int):void
    {
        for (var i:int = 0; i < initialCount; i++)
            pool.push(createInstance());
    }

    /**
     *  @private
     */
    private function reportSize():void
    {
        if (lastReportedSize == pool.length)
            return;

        dispatchEvent(new Event(SIZE_CHANGED));
        lastReportedSize = pool.length;

    }

    /**
     *  @private
     *  Updates the pool whenever the <code>highWatermark</code>,
     *  <code>properties</code> properties change.
     */
    private function updatePool():void
    {

        if (!_generator || !pool)
            return;

        if (highWatermarkChanged)
        {
            if (_highWatermark > -1 && _highWatermark < pool.length)
                pool.splice(_highWatermark);

            reportSize();
            highWatermarkChanged = false;
        }

        if (propertiesChanged)
        {
            var n:int = pool.length;
            for (var i:int = 0; i < n; i++)
                pool[i] = resetObject(pool[i]);

            propertiesChanged = false;
        }
    }
}
}
